package com.yvan.serverless.mybatis;

import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.SystemClock;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.MapperRegistry;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.ResourceUtils;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Stream;

@Slf4j
public class MapperRefresh implements Runnable {

    private static Exception xmlException = null;

    public static Exception getXmlException() {
        return xmlException;
    }

    /**
     * 记录jar包存在的mapper
     */
    private final SqlSessionFactory sqlSessionFactory;
    private Resource[] mapperLocations;
    /**
     * 是否开启刷新mapper
     */
    private final boolean enabled;
    private Long beforeTime = 0L;
    private Configuration configuration;
    /**
     * xml文件目录
     */
    private Set<String> fileSet;
    // 记录上次的文件目录，做对比
    private volatile Set<String> lastFileSet;
    /**
     * 延迟加载时间
     */
    private int delaySeconds = 1;
    /**
     * 刷新间隔时间
     */
    private int sleepSeconds = 1;

    private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();

    private MybatisPlusProperties mybatisPlusProperties;


    public MapperRefresh(MybatisPlusProperties mybatisPlusProperties, SqlSessionFactory sqlSessionFactory, int delaySeconds,
                         int sleepSeconds, boolean enabled) {
        this.mybatisPlusProperties = mybatisPlusProperties;
        this.sqlSessionFactory = sqlSessionFactory;
        this.delaySeconds = delaySeconds;
        this.enabled = enabled;
        this.sleepSeconds = sleepSeconds;
        this.configuration = sqlSessionFactory.getConfiguration();
        this.run();
    }

    public MapperRefresh(MybatisPlusProperties mybatisPlusProperties, SqlSessionFactory sqlSessionFactory, boolean enabled) {
        this.mybatisPlusProperties = mybatisPlusProperties;
        this.sqlSessionFactory = sqlSessionFactory;
        this.enabled = enabled;
        this.configuration = sqlSessionFactory.getConfiguration();
        this.run();
    }

    @Override
    public void run() {
        /*
         * 启动 XML 热加载
         */
        if (enabled) {
            beforeTime = SystemClock.now();
            final MapperRefresh runnable = this;
            new Thread(() -> {
                if (fileSet == null) {
                    fileSet = new HashSet<>();
                }
                if (lastFileSet == null) {
                    lastFileSet = new HashSet<>();
                }
                try {
                    Thread.sleep(delaySeconds * 1000);
                } catch (InterruptedException interruptedException) {
                    interruptedException.printStackTrace();
                }
                do {

                    // 获取当前的xml文件
                    Resource[] resources = Stream.of(Optional.ofNullable(mybatisPlusProperties.getMapperLocations()).orElse(new String[0]))
                            .flatMap(location -> {
                                try {
                                    return Stream.of(resourceResolver.getResources(location));
                                } catch (IOException e) {
                                    throw new RuntimeException(e);
                                }
                            })
                            .toArray(Resource[]::new);
                    //
                    mapperLocations = resources;
                    fileSet.clear();
                    Set<String> rSet = new HashSet<>();
                    rSet.addAll(lastFileSet);
                    if (mapperLocations != null) {
                        for (Resource mapperLocation : mapperLocations) {
                            try {
                                String key = "";
                                if (ResourceUtils.isJarURL(mapperLocation.getURL())) {
                                    key = new UrlResource(ResourceUtils.extractJarFileURL(mapperLocation.getURL()))
                                            .getFile().getPath();
                                } else {
                                    key = mapperLocation.getFile().getPath();
                                }
                                fileSet.add(key);
                                if (!lastFileSet.contains(key)) {
                                    rSet.add(key);
                                } else {
                                    rSet.remove(key);
                                }
                            } catch (IOException ioException) {
                                ioException.printStackTrace();
                            }
                        }
                    }
                    try {
                        for (String filePath : fileSet) {
                            File file = new File(filePath);
                            if (file.isFile() && file.lastModified() > beforeTime) {
                                rSet.add(filePath);
                            }
                        }
                        for (String filePath : rSet) {
                            runnable.refresh(new FileSystemResource(new File(filePath)));
                            xmlException = null;
                        }
                        beforeTime = SystemClock.now();
                        /** 清空异常 **/

                    } catch (Exception exception) {
                        /** 解析异常也需要更新最后刷新时间 避免一直报错 **/
                        beforeTime = SystemClock.now();
                        xmlException = exception;
                        exception.printStackTrace();
                    }
                    lastFileSet.clear();
                    lastFileSet.addAll(fileSet);
                    try {
                        Thread.sleep(sleepSeconds * 1000);
                    } catch (InterruptedException interruptedException) {
                        interruptedException.printStackTrace();
                    }

                } while (true);
            }, "mybatis-plus MapperRefresh").start();
        }
    }

    /**
     * 刷新mapper
     */
    @SuppressWarnings("rawtypes")
    private void refresh(Resource resource) throws NoSuchFieldException, IllegalAccessException {
        this.configuration = sqlSessionFactory.getConfiguration();
        boolean isSupper = configuration.getClass().getSuperclass() == Configuration.class;
        try {
            Field loadedResourcesField = isSupper ? configuration.getClass().getSuperclass().getDeclaredField("loadedResources")
                    : configuration.getClass().getDeclaredField("loadedResources");
            loadedResourcesField.setAccessible(true);
            Set loadedResourcesSet = ((Set) loadedResourcesField.get(configuration));
            XPathParser xPathParser = new XPathParser(resource.getInputStream(), true, configuration.getVariables(),
                    new XMLMapperEntityResolver());
            XNode context = xPathParser.evalNode("/mapper");
            String namespace = context.getStringAttribute("namespace");
            Field field = MapperRegistry.class.getDeclaredField("knownMappers");
            field.setAccessible(true);
            Map mapConfig = (Map) field.get(configuration.getMapperRegistry());
            try {
                mapConfig.remove(Resources.classForName(namespace));
            } catch (ClassNotFoundException e) {
            }
            loadedResourcesSet.remove(resource.toString());
            configuration.getCacheNames().remove(namespace);
            cleanParameterMap(context.evalNodes("/mapper/parameterMap"), namespace);
            cleanResultMap(context.evalNodes("/mapper/resultMap"), namespace);
            cleanKeyGenerators(context.evalNodes("insert|update"), namespace);
            cleanSqlElement(context.evalNodes("/mapper/sql"), namespace);

            Set<String> needClearSet = new HashSet<>();
            Iterator<MappedStatement> it = sqlSessionFactory.getConfiguration().getMappedStatements().iterator();
            while (it.hasNext()) {
                Object object = it.next();
                if (object instanceof MappedStatement) {
                    MappedStatement statement = (MappedStatement) object;
                    if (resource.toString().equals(statement.getResource())) {
                        needClearSet.add(statement.getId());
                    }
                }
            }
//            sqlSessionFactory.getConfiguration().getMappedStatementNames().forEach((id) -> {
//                MappedStatement statement = sqlSessionFactory.getConfiguration().getMappedStatement(id);
//                if (resource.toString().equals(statement.getResource())) {
//                    // sqlSessionFactory.getConfiguration().getMappedStatements()
//                    needClearSet.add(id);
//                }
//            });
            Field mappedStatementsField = Configuration.class.getDeclaredField("mappedStatements");
            mappedStatementsField.setAccessible(true);
            HashMap<String, Object> mappedStatements = (HashMap<String, Object>) mappedStatementsField.get(sqlSessionFactory.getConfiguration());
            needClearSet.forEach((k) -> mappedStatements.remove(k));

            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(resource.getInputStream(),
                    sqlSessionFactory.getConfiguration(),
                    resource.toString(), sqlSessionFactory.getConfiguration().getSqlFragments());
            xmlMapperBuilder.parse();
            log.info("refresh: '" + resource + "', success!");

        } catch (IOException e) {
            log.error("Refresh " + resource + "IOException :" + e.getMessage(), e);

        } finally {
            ErrorContext.instance().reset();
        }
    }

    /**
     * 清理parameterMap
     *
     * @param list      ignore
     * @param namespace ignore
     */
    private void cleanParameterMap(List<XNode> list, String namespace) {
        for (XNode parameterMapNode : list) {
            String id = parameterMapNode.getStringAttribute("id");
            configuration.getParameterMaps().remove(namespace + StringPool.DOT + id);
        }
    }

    /**
     * 清理resultMap
     *
     * @param list      ignore
     * @param namespace ignore
     */
    private void cleanResultMap(List<XNode> list, String namespace) {
        for (XNode resultMapNode : list) {
            String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
            configuration.getResultMapNames().remove(id);
            configuration.getResultMapNames().remove(namespace + StringPool.DOT + id);
            clearResultMap(resultMapNode, namespace);
        }
    }

    private void clearResultMap(XNode xNode, String namespace) {
        for (XNode resultChild : xNode.getChildren()) {
            if ("association".equals(resultChild.getName()) || "collection".equals(resultChild.getName())
                    || "case".equals(resultChild.getName())) {
                if (resultChild.getStringAttribute("select") == null) {
                    configuration.getResultMapNames().remove(
                            resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
                    configuration.getResultMapNames().remove(
                            namespace + StringPool.DOT + resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
                    if (resultChild.getChildren() != null && !resultChild.getChildren().isEmpty()) {
                        clearResultMap(resultChild, namespace);
                    }
                }
            }
        }
    }

    /**
     * 清理selectKey
     *
     * @param list      ignore
     * @param namespace ignore
     */
    private void cleanKeyGenerators(List<XNode> list, String namespace) {
        for (XNode context : list) {
            String id = context.getStringAttribute("id");
            configuration.getKeyGeneratorNames().remove(id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
            configuration.getKeyGeneratorNames().remove(namespace + StringPool.DOT + id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
        }
    }

    /**
     * 清理sql节点缓存
     *
     * @param list      ignore
     * @param namespace ignore
     */
    private void cleanSqlElement(List<XNode> list, String namespace) {
        for (XNode context : list) {
            String id = context.getStringAttribute("id");
            configuration.getSqlFragments().remove(id);
            configuration.getSqlFragments().remove(namespace + StringPool.DOT + id);
        }
    }
}

